#!/usr/bin/env python3
"""
AI DevFlow Unified CLI

Provides Python implementations of common script behaviors for project-side
and framework-side repositories. Mirrors native PowerShell scripts to support
web-based or cross-platform invocation. Mode is config-driven via
`config/rules.env`.

Usage examples:
  python scripts/cli.py render-rules
  python scripts/cli.py check-ai-compliance
  python scripts/cli.py install-git-hooks

  Key commands:
  render-rules, check-ai-compliance, check-architecture, check-cross-model-ci,
  check-doc-sync, check-injection, check-session-recovery, check-task-docs,
  check-templates, check-transaction, cross-model-guard, framework-evolve,
  gen-mapping-doc, refactor-rules, verify-rule-linkages,
  list-interface-relations, quick-commit, validate-commit-msg,
  setup-framework-mode, install-git-hooks, dev-templates

Notes:
  - Exits with non-zero code on failures to facilitate CI usage.
  - Designed to run from repo root or any location; path detection uses script location.
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List
import fnmatch


def detect_root(script_path: Path) -> Path:
    # Expected location: <Repo>/scripts/cli.py
    return script_path.parent.parent


def load_env(env_file: Path) -> Dict[str, str]:
    kv: Dict[str, str] = {}
    if not env_file.exists():
        return kv
    for line in env_file.read_text(encoding="utf-8", errors="ignore").splitlines():
        t = line.strip()
        if not t or t.startswith("#"):
            continue
        if "=" in t:
            key, val = t.split("=", 1)
            kv[key] = val
    return kv


def save_env(env_file: Path, updates: Dict[str, str]) -> None:
    env_file.parent.mkdir(parents=True, exist_ok=True)
    lines: List[str] = []
    existing = load_env(env_file)
    existing.update(updates)
    for k, v in existing.items():
        lines.append(f"{k}={v}")
    env_file.write_text("\n".join(lines) + "\n", encoding="utf-8")


def cmd_render_rules(root: Path) -> int:
    env_file = root / "config" / "rules.env"
    src_dir = root / "rules"
    out_dir = root / ".tmp" / "rendered_rules"
    kv = load_env(env_file)
    count = 0
    for src in src_dir.rglob("*.md"):
        content = src.read_text(encoding="utf-8", errors="ignore")
        for k, v in kv.items():
            content = content.replace(f"{{{{{k}}}}}", v)
        rel = src.relative_to(src_dir)
        dst = out_dir / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        dst.write_text(content, encoding="utf-8")
        count += 1
    print(f"[render-rules] env={env_file} keys={','.join(kv.keys())} count={count}")
    return 0


def cmd_check_ai_compliance(root: Path) -> int:
    required = [
        root / "rules" / "ai_rules_and_prompts.md",
        root / "rules" / "project_rules.md",
        root / "docs" / "standard_prompts.md",
    ]
    missing = [str(p) for p in required if not p.exists()]
    todo_count = 0
    fixme_count = 0
    for base in [root / "rules", root / "docs"]:
        for f in base.rglob("*.md"):
            t = f.read_text(encoding="utf-8", errors="ignore")
            todo_count += len(re.findall(r"TODO", t))
            fixme_count += len(re.findall(r"FIXME", t))
    if missing or todo_count or fixme_count:
        print("[ai-compliance] issues found:")
        if missing:
            print("  missing:")
            for m in missing:
                print(f"    - {m}")
        print(f"  markers: TODO={todo_count}, FIXME={fixme_count}")
        return 2
    print("[ai-compliance] OK")
    return 0


def cmd_check_architecture(root: Path) -> int:
    required = [
        root / "src" / "main" / "java",
        root / "src" / "main" / "python",
        root / "src" / "main" / "resources",
        root / "rules" / "ddd_design_guide.md",
        root / "rules" / "code_design_rules.md",
    ]
    missing = [str(p) for p in required if not p.exists()]
    if missing:
        print("[architecture] missing components:")
        for m in missing:
            print(f"  - {m}")
        return 2
    print("[architecture] OK")
    return 0


def cmd_check_cross_model_ci(root: Path) -> int:
    ci = root / ".github" / "workflows" / "ci.yml"
    if not ci.exists():
        print(f"[cross-model-ci] missing {ci}")
        return 2
    t = ci.read_text(encoding="utf-8", errors="ignore")
    if not ("build" in t and "test" in t):
        print("[cross-model-ci] expected 'build' and 'test' keywords")
        return 2
    print("[cross-model-ci] OK")
    return 0


def cmd_check_doc_sync(root: Path, apply: bool = False) -> int:
    meta = root / "docs" / "meta_index.md"
    entries = ["rules/project_rules.md", "docs/framework_changes.md"]
    changed = False
    existing = meta.read_text(encoding="utf-8", errors="ignore") if meta.exists() else ""
    for e in entries:
        if e not in existing:
            print(f"[doc-sync] missing index entry: {e}")
            if apply:
                existing += f"\n- {e}\n"
                changed = True
    if apply and changed:
        meta.parent.mkdir(parents=True, exist_ok=True)
        meta.write_text(existing, encoding="utf-8")
        print(f"[doc-sync] applied updates -> {meta}")
    return 0 if not (changed and not apply) else 2


def cmd_check_injection(root: Path) -> int:
    suspicious_patterns = [
        r"\beval\(", r"\bexec\(", r"os\.system\(", r"subprocess\.Popen\(",
        r"Runtime\.getRuntime\(\)\.exec\(",
    ]
    findings: List[str] = []
    for ext in (".py", ".java"):
        for f in root.rglob(f"*{ext}"):
            t = f.read_text(encoding="utf-8", errors="ignore")
            for pat in suspicious_patterns:
                if re.search(pat, t):
                    findings.append(str(f))
                    break
    if findings:
        print("[injection] potential risky calls:")
        for f in findings[:50]:
            print(f"  - {f}")
        return 2
    print("[injection] OK")
    return 0


def cmd_check_session_recovery(root: Path) -> int:
    env_file = root / "config" / "rules.env"
    guide = root / "docs" / "ai_initialization_guide.md"
    env = load_env(env_file)
    ok = "INIT_TIMESTAMP" in env and guide.exists()
    if not ok:
        print(f"[session-recovery] missing INIT_TIMESTAMP or guide {guide}")
        return 2
    print("[session-recovery] OK")
    return 0


def cmd_check_task_docs(root: Path) -> int:
    idx = root / "rules" / "tasks_index.md"
    milestones = root / "docs" / "milestones.md"
    if not idx.exists() or not milestones.exists():
        print("[task-docs] missing tasks_index.md or milestones.md")
        return 2
    t = milestones.read_text(encoding="utf-8", errors="ignore")
    if "tasks_index" not in t:
        print("[task-docs] milestones.md should reference tasks_index.md")
        return 2
    print("[task-docs] OK")
    return 0


def cmd_check_templates(root: Path) -> int:
    required_dirs = ["python", "java", "mvc", "trae"]
    index = root / "templates" / "INDEX.md"
    missing = [d for d in required_dirs if not (root / "templates" / d).exists()]
    if not index.exists() or missing:
        print("[templates] missing INDEX.md or template subdirs:")
        for d in missing:
            print(f"  - templates/{d}")
        return 2
    print("[templates] OK")
    return 0


def cmd_check_transaction(root: Path) -> int:
    sql_dir = root / "file" / "sql"
    missing_begin = 0
    missing_commit = 0
    for f in sql_dir.rglob("*.sql"):
        t = f.read_text(encoding="utf-8", errors="ignore")
        has_begin = re.search(r"\bBEGIN\b", t, re.IGNORECASE)
        has_commit = re.search(r"\bCOMMIT\b", t, re.IGNORECASE)
        if not has_begin:
            missing_begin += 1
        if not has_commit:
            missing_commit += 1
    if missing_begin or missing_commit:
        print(f"[transaction] files missing BEGIN={missing_begin}, COMMIT={missing_commit}")
        return 2
    print("[transaction] OK")
    return 0


def cmd_cross_model_guard(root: Path) -> int:
    guide = root / "rules" / "model_routing_guide.md"
    meta = root / "docs" / "meta_index.md"
    if not guide.exists():
        print(f"[cross-model-guard] missing {guide}")
        return 2
    t = meta.read_text(encoding="utf-8", errors="ignore") if meta.exists() else ""
    if "model_routing_guide.md" not in t:
        print("[cross-model-guard] meta_index should reference model_routing_guide.md")
        return 2
    print("[cross-model-guard] OK")
    return 0


def cmd_framework_evolve(root: Path, mode: str) -> int:
    if mode not in {"verify", "apply"}:
        print("[framework-evolve] mode must be verify|apply")
        return 2
    # Verify required docs are present
    required = [
        root / "docs" / "framework_changes.md",
        root / "rules" / "project_rules.md",
        root / "rules" / "framework_info.md",
    ]
    missing = [str(p) for p in required if not p.exists()]
    if missing:
        print("[framework-evolve] missing:")
        for m in missing:
            print(f"  - {m}")
        return 2
    if mode == "apply":
        env_file = root / "config" / "rules.env"
        save_env(env_file, {"FRAMEWORK_STATUS": "paired"})
        print(f"[framework-evolve] applied status 'paired' -> {env_file}")
    else:
        print("[framework-evolve] verify OK")
    return 0


def cmd_gen_mapping_doc(root: Path) -> int:
    out = root / ".tmp" / "rules_extracted.md"
    lines: List[str] = ["# Rules Mapping Extract", ""]
    for f in (root / "rules").rglob("*.md"):
        title = ""
        for line in f.read_text(encoding="utf-8", errors="ignore").splitlines():
            if line.startswith("#"):
                title = line.strip().lstrip("# ")
                break
        lines.append(f"- {f.relative_to(root)}: {title}")
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"[gen-mapping-doc] -> {out}")
    return 0


def cmd_refactor_rules(root: Path) -> int:
    out = root / ".tmp" / "rules_refactor_plan.md"
    lines: List[str] = ["# Rules Refactor Plan (Draft)", ""]
    for f in (root / "rules").rglob("*.md"):
        title = ""
        for line in f.read_text(encoding="utf-8", errors="ignore").splitlines():
            if line.startswith("#"):
                title = line.strip().lstrip("# ")
                break
        rel = f.relative_to(root)
        lines.append(f"## {title} ({rel})")
        lines.append("- Normalize headings, add TOC, unify terminology")
        lines.append("- Add usage notes and cross-links to meta_index.md")
        lines.append("- Extract actionable checklists where applicable\n")
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"[refactor-rules] -> {out}")
    return 0


def cmd_verify_rule_linkages(root: Path) -> int:
    meta = root / "docs" / "meta_index.md"
    prompts = root / "docs" / "prompts_index.md"
    ok_count = 0
    if meta.exists():
        t = meta.read_text(encoding="utf-8", errors="ignore")
        if "rules/project_rules.md" in t and "rules/framework_info.md" in t:
            ok_count += 1
    if prompts.exists():
        t = prompts.read_text(encoding="utf-8", errors="ignore")
        if "ai_rules_and_prompts.md" in t:
            ok_count += 1
    if ok_count < 2:
        print("[verify-rule-linkages] missing cross references in meta/prompts indexes")
        return 2
    print("[verify-rule-linkages] OK")
    return 0


def cmd_list_interface_relations(root: Path) -> int:
    out_dir = root / ".reports" / "interface_relations"
    out_dir.mkdir(parents=True, exist_ok=True)
    out = out_dir / "interfaces.txt"
    lines: List[str] = []
    for f in (root / "src" / "main" / "java").rglob("*.java"):
        t = f.read_text(encoding="utf-8", errors="ignore")
        if "interface " in t:
            lines.append(str(f.relative_to(root)))
    out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"[list-interface-relations] -> {out} count={len(lines)}")
    return 0


def cmd_quick_commit(root: Path, msg: str) -> int:
    if not msg:
        msg = "chore: quick commit"
    try:
        subprocess.check_call(["git", "add", "-A"], cwd=root)
        subprocess.check_call(["git", "commit", "-m", msg], cwd=root)
    except subprocess.CalledProcessError:
        print("[quick-commit] git commit failed")
        return 2
    print("[quick-commit] committed")
    return 0


def validate_conventional(msg: str) -> bool:
    pat = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([^)]+\))?: .+"
    return re.match(pat, msg) is not None


def cmd_validate_commit_msg(root: Path, msg: str | None, file: Path | None) -> int:
    if file and file.exists():
        msg = file.read_text(encoding="utf-8", errors="ignore").strip().splitlines()[0]
    if not msg:
        print("[validate-commit-msg] missing message")
        return 2
    if not validate_conventional(msg):
        print(f"[validate-commit-msg] invalid: {msg}")
        return 2
    print("[validate-commit-msg] OK")
    return 0


def cmd_setup_framework_mode(root: Path) -> int:
    env_file = root / "config" / "rules.env"
    save_env(env_file, {"FRAMEWORK_STATUS": "paired"})
    print(f"[setup-framework-mode] FRAMEWORK_STATUS=paired -> {env_file}")
    return 0


def cmd_install_git_hooks(root: Path) -> int:
    hooks = root / ".git" / "hooks"
    hooks.mkdir(parents=True, exist_ok=True)
    pre_commit = hooks / "pre-commit"
    commit_msg = hooks / "commit-msg"
    def _load_tpl(name: str) -> str | None:
        p = root / "scripts" / "hooks" / f"{name}.hook.sh"
        if p.exists():
            try:
                return p.read_text(encoding="utf-8", errors="ignore")
            except Exception:
                return None
        return None

    pre_tpl = _load_tpl("pre-commit")
    if not pre_tpl:
        pre_tpl = (
            "#!/usr/bin/env bash\n"
            "set -e\n"
            "ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\n"
            "SCRIPT=\"\";\n"
            "candidate=\"$ROOT/scripts/windows/pre-commit-check.ps1\"\n"
            "if [ -f \"$candidate\" ]; then SCRIPT=\"$candidate\"; fi\n"
            "if [ -z \"$SCRIPT\" ]; then\n"
            "  for d in \"$ROOT\"/*; do\n"
            "    if [ -f \"$d/scripts/windows/pre-commit-check.ps1\" ]; then SCRIPT=\"$d/scripts/windows/pre-commit-check.ps1\"; break; fi\n"
            "  done\n"
            "fi\n"
            "if [ -z \"$SCRIPT\" ] && [ -f \"$ROOT/config/rules.env\" ]; then\n"
            "  MIRROR_DIR_NAME=\"$(grep -E '^MIRROR_DIR_NAME=' \"$ROOT/config/rules.env\" | sed 's/^MIRROR_DIR_NAME=//')\"\n"
            "  if [ -n \"$MIRROR_DIR_NAME\" ] && [ -f \"$ROOT/$MIRROR_DIR_NAME/scripts/windows/pre-commit-check.ps1\" ]; then\n"
            "    SCRIPT=\"$ROOT/$MIRROR_DIR_NAME/scripts/windows/pre-commit-check.ps1\";\n"
            "  fi\n"
            "fi\n"
            "if [ -z \"$SCRIPT\" ]; then echo \"[hook] pre-commit-check.ps1 not found\" >&2; exit 1; fi\n"
            "pwsh -NoProfile -File \"$SCRIPT\"\n"
        )
    pre_commit.write_text(pre_tpl, encoding="utf-8")
    msg_tpl = _load_tpl("commit-msg")
    if not msg_tpl:
        msg_tpl = (
            "#!/usr/bin/env bash\n"
            "set -e\n"
            "ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\n"
            "SCRIPT=\"\";\n"
            "candidate=\"$ROOT/scripts/windows/validate-commit-msg.ps1\"\n"
            "if [ -f \"$candidate\" ]; then SCRIPT=\"$candidate\"; fi\n"
            "if [ -z \"$SCRIPT\" ]; then\n"
            "  for d in \"$ROOT\"/*; do\n"
            "    if [ -f \"$d/scripts/windows/validate-commit-msg.ps1\" ]; then SCRIPT=\"$d/scripts/windows/validate-commit-msg.ps1\"; break; fi\n"
            "  done\n"
            "fi\n"
            "if [ -z \"$SCRIPT\" ] && [ -f \"$ROOT/config/rules.env\" ]; then\n"
            "  MIRROR_DIR_NAME=\"$(grep -E '^MIRROR_DIR_NAME=' \"$ROOT/config/rules.env\" | sed 's/^MIRROR_DIR_NAME=//')\"\n"
            "  if [ -n \"$MIRROR_DIR_NAME\" ] && [ -f \"$ROOT/$MIRROR_DIR_NAME/scripts/windows/validate-commit-msg.ps1\" ]; then\n"
            "    SCRIPT=\"$ROOT/$MIRROR_DIR_NAME/scripts/windows/validate-commit-msg.ps1\";\n"
            "  fi\n"
            "fi\n"
            "if [ -z \"$SCRIPT\" ]; then echo \"[hook] validate-commit-msg.ps1 not found\" >&2; exit 1; fi\n"
            "pwsh -NoProfile -File \"$SCRIPT\" -MsgFile \"$1\"\n"
        )
    commit_msg.write_text(msg_tpl, encoding="utf-8")
    for f in (pre_commit, commit_msg):
        try:
            subprocess.check_call(["chmod", "+x", str(f)])
        except Exception:
            pass
    print(f"[install-git-hooks] installed -> {hooks}")
    return 0


def cmd_dev_templates(root: Path, lang: str, style: str, target: Path | None) -> int:
    src = root / "templates" / (lang or "python")
    if not src.exists():
        print(f"[dev-templates] missing templates for {lang}")
        return 2
    dst = target or (root / ".tmp" / "dev_templates")
    dst.mkdir(parents=True, exist_ok=True)
    for f in src.rglob("*"):
        if f.is_file():
            rel = f.relative_to(src)
            dd = dst / rel
            dd.parent.mkdir(parents=True, exist_ok=True)
            dd.write_text(f.read_text(encoding="utf-8", errors="ignore"), encoding="utf-8")
    print(f"[dev-templates] copied {lang} -> {dst}")
    return 0


def detect_java_style(root: Path, source_dirs: List[Path] | None = None) -> Dict[str, str | int | bool]:
    src_dir_default = root / "src" / "main" / "java"
    dirs: List[Path] = []
    if source_dirs:
        dirs = [d if d.is_absolute() else (root / d) for d in source_dirs]
    else:
        dirs = [src_dir_default]
    pkg_counts: Dict[str, int] = {}
    flags = {"rest_controller": False, "controller": False, "service": False, "repository": False}
    indent_step = 4
    any_java = False
    for src_dir in dirs:
        if not src_dir.exists():
            continue
        any_java = True
        for f in src_dir.rglob("*.java"):
            t = f.read_text(encoding="utf-8", errors="ignore")
            # package
            m = re.search(r"^\s*package\s+([\w\.]+);", t, re.MULTILINE)
            if m:
                pkg = m.group(1)
                pkg_counts[pkg] = pkg_counts.get(pkg, 0) + 1
            # annotations
            if "@RestController" in t:
                flags["rest_controller"] = True
            if "@Controller" in t:
                flags["controller"] = True
            if "@Service" in t:
                flags["service"] = True
            if "@Repository" in t:
                flags["repository"] = True
            # indent heuristic
            for line in t.splitlines():
                if line.startswith(" ") and not line.strip().startswith("//") and "{" in line:
                    s = len(line) - len(line.lstrip(" "))
                    if s in (2, 4):
                        indent_step = s
                        break
    package = max(pkg_counts, key=pkg_counts.get) if pkg_counts else "com.example"
    return {"package": package, **flags, "indent": indent_step}


def write_java_style_templates(root: Path, out_dir: Path, style: Dict[str, str | int | bool]) -> None:
    pkg = str(style.get("package", "com.example"))
    indent = int(style.get("indent", 4))
    use_rest = bool(style.get("rest_controller", False))
    use_ctrl = bool(style.get("controller", True))
    use_service = bool(style.get("service", True))
    use_repo = bool(style.get("repository", True))

    out_dir.mkdir(parents=True, exist_ok=True)

    def w(name: str, content: str) -> None:
        (out_dir / name).write_text(content, encoding="utf-8")

    sp = " " * indent
    header = (
        "/*\n"
        " Generated style template. Placeholders:\n"
        "  - {{PACKAGE}} base package\n"
        "  - {{ClassName}} class name\n"
        "  - {{AUTHOR}} / {{DATE}} optional\n"
        "*/\n"
    )

    ctrl_anno = "@RestController" if use_rest else ("@Controller" if use_ctrl else "")
    controller = (
        f"{header}package {{PACKAGE}};\n\n"
        f"{ctrl_anno}\n"
        f"public class {{ClassName}}Controller {{\n"
        f"{sp}public {{ClassName}}Controller() {{}}\n"
        f"}}\n"
    )
    service_anno_line = "@Service\n" if use_service else ""
    service = (
        f"{header}package {{PACKAGE}};\n\n"
        f"{service_anno_line}"
        f"public class {{ClassName}}Service {{\n"
        f"{sp}public {{ClassName}}Service() {{}}\n"
        f"}}\n"
    )
    repository_anno_line = "@Repository\n" if use_repo else ""
    repository = (
        f"{header}package {{PACKAGE}};\n\n"
        f"{repository_anno_line}"
        f"public interface {{ClassName}}Repository {{\n"
        f"}}\n"
    )
    entity = (
        f"{header}package {{PACKAGE}};\n\n"
        f"public class {{ClassName}} {{\n"
        f"{sp}// TODO: add fields\n"
        f"}}\n"
    )
    dto = (
        f"{header}package {{PACKAGE}};\n\n"
        f"public class {{ClassName}}Dto {{\n"
        f"{sp}// TODO: add fields\n"
        f"}}\n"
    )
    vo = (
        f"{header}package {{PACKAGE}};\n\n"
        f"public class {{ClassName}}Vo {{\n"
        f"{sp}// TODO: add fields\n"
        f"}}\n"
    )

    w("Controller.java.tmpl", controller)
    w("Service.java.tmpl", service)
    w("Repository.java.tmpl", repository)
    w("Entity.java.tmpl", entity)
    w("Dto.java.tmpl", dto)
    w("Vo.java.tmpl", vo)


def write_python_style_templates(root: Path, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    def w(rel: str, content: str) -> None:
        p = out_dir / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(content, encoding="utf-8")

    header = (
        '"""\n'
        "Generated style template. Placeholders:\n"
        "  - {{ModuleName}} / {{ClassName}}\n"
        '"""\n\n'
    )
    w("ddd/application/service.py.tmpl", header + "class {{ClassName}}Service:\n    def __init__(self):\n        pass\n")
    w("ddd/domain/model.py.tmpl", header + "class {{ClassName}}:\n    def __init__(self):\n        pass\n")
    w("ddd/infrastructure/repository.py.tmpl", header + "class {{ClassName}}Repository:\n    def save(self, obj):\n        raise NotImplementedError\n")
    w("ddd/interfaces/controller.py.tmpl", header + "class {{ClassName}}Controller:\n    def handle(self, req):\n        pass\n")


def cmd_gen_style_templates(root: Path, lang: str, apply: bool) -> int:
    out_base = root / ".tmp" / "style_templates"
    if lang == "java":
        style = detect_java_style(root)
        out_dir = out_base / "java"
        write_java_style_templates(root, out_dir, style)
        print(f"[gen-style-templates] java -> {out_dir} package={style['package']}")
        if apply:
            target = root / "templates" / "java" / "style"
            target.mkdir(parents=True, exist_ok=True)
            for f in out_dir.glob("*.tmpl"):
                (target / f.name).write_text(f.read_text(encoding="utf-8", errors="ignore"), encoding="utf-8")
            print(f"[gen-style-templates] applied -> {target}")
    elif lang == "python":
        out_dir = out_base / "python"
        write_python_style_templates(root, out_dir)
        print(f"[gen-style-templates] python -> {out_dir}")
        if apply:
            target = root / "templates" / "python" / "style"
            for f in out_dir.rglob("*.tmpl"):
                rel = f.relative_to(out_dir)
                dst = target / rel
                dst.parent.mkdir(parents=True, exist_ok=True)
                dst.write_text(f.read_text(encoding="utf-8", errors="ignore"), encoding="utf-8")
            print(f"[gen-style-templates] applied -> {target}")
    else:
        print("[gen-style-templates] unsupported lang. use: java|python")
        return 2
    return 0


def apply_placeholders(out_dir: Path, placeholders: Dict[str, str]) -> None:
    if not out_dir.exists():
        return
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    ph = dict(placeholders or {})
    for k, v in list(ph.items()):
        if isinstance(v, str) and v == "{{NOW}}":
            ph[k] = now
    for f in out_dir.rglob("*.tmpl"):
        t = f.read_text(encoding="utf-8", errors="ignore")
        for k, v in ph.items():
            t = t.replace(f"{{{{{k}}}}}", str(v))
        f.write_text(t, encoding="utf-8")


def write_mimic_templates(root: Path, out_dir: Path, source_dirs: List[Path], include_globs: List[str] | None, exclude_globs: List[str] | None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    inc = include_globs or []
    exc = exclude_globs or []
    def match_any(p: Path, patterns: List[str]) -> bool:
        if not patterns:
            return False
        s = str(p) if p.is_absolute() else str(p)
        for pat in patterns:
            if fnmatch.fnmatch(s, pat):
                return True
        return False
    default_exts = (".java", ".py", ".js", ".jsx", ".ts", ".tsx", ".vue", ".css", ".scss", ".sass", ".html")
    for d in (source_dirs or [root / "src"]):
        dd = d if d.is_absolute() else (root / d)
        if not dd.exists():
            continue
        for f in dd.rglob("*"):
            if not f.is_file():
                continue
            # Exclude first
            if match_any(f, exc):
                continue
            # Include filter
            if inc:
                if not match_any(f, inc):
                    continue
            else:
                if f.suffix not in default_exts:
                    continue
            rel = f.relative_to(dd)
            dst = out_dir / rel
            dst = dst.with_name(dst.name + ".tmpl")
            dst.parent.mkdir(parents=True, exist_ok=True)
            try:
                content = f.read_text(encoding="utf-8", errors="ignore")
            except Exception:
                # Binary or unreadable; skip
                continue
            dst.write_text(content, encoding="utf-8")


def cmd_gen_templates_by_rules(root: Path) -> int:
    import json
    rules_path = root / "config" / "template_gen_rules.json"
    if not rules_path.exists():
        print(f"[gen-templates-by-rules] missing rules file: {rules_path}")
        print("  Create it with schema: { version, jobs:[{language, family, source_dirs, output_dir, apply, apply_dir, placeholders}] }")
        return 2
    try:
        data = json.loads(rules_path.read_text(encoding="utf-8", errors="ignore"))
    except Exception as e:
        print(f"[gen-templates-by-rules] parse error: {e}")
        return 2
    jobs = data.get("jobs", [])
    if not jobs:
        print("[gen-templates-by-rules] no jobs defined")
        return 2
    for job in jobs:
        # mimic mode: copy files from source_dirs into out_dir and turn into .tmpl
        mode = str(job.get("mode", "")).lower()
        mimic_flag = bool(job.get("mimic", False)) or mode == "mimic"
        lang = job.get("language")
        out_rel = job.get("output_dir") or f".tmp/style_templates/{lang}"
        out_dir = root / out_rel
        apply_flag = bool(job.get("apply", False))
        apply_rel = job.get("apply_dir") or f"templates/{lang}/style"
        apply_dir = root / apply_rel
        placeholders = job.get("placeholders") or {}
        source_dirs = [Path(p) for p in job.get("source_dirs", [])]
        if mimic_flag:
            inc = job.get("include") or job.get("include_globs")
            exc = job.get("exclude") or job.get("exclude_globs")
            # sensible defaults by language
            if not inc:
                if lang in ("js", "javascript", "ts", "typescript"):
                    inc = ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.vue", "**/*.css", "**/*.scss", "**/*.sass"]
                elif lang == "java":
                    inc = ["**/*.java"]
                elif lang == "python":
                    inc = ["**/*.py"]
                else:
                    inc = ["**/*.java", "**/*.py", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.vue"]
            write_mimic_templates(root, out_dir, source_dirs or [root / "src"], inc, exc or [])
            apply_placeholders(out_dir, placeholders)
            print(f"[gen-templates-by-rules] generated -> {out_dir}")
            if apply_flag:
                for f in out_dir.rglob("*.tmpl"):
                    rel = f.relative_to(out_dir)
                    dst = apply_dir / rel
                    dst.parent.mkdir(parents=True, exist_ok=True)
                    dst.write_text(f.read_text(encoding="utf-8", errors="ignore"), encoding="utf-8")
                print(f"[gen-templates-by-rules] applied -> {apply_dir}")
            continue
        if lang == "java":
            style = detect_java_style(root, source_dirs or None)
            write_java_style_templates(root, out_dir, style)
            ph = {"PACKAGE": style.get("package", "com.example")}
            ph.update(placeholders)
            apply_placeholders(out_dir, ph)
        elif lang == "python":
            write_python_style_templates(root, out_dir)
            apply_placeholders(out_dir, placeholders)
        elif lang in ("js", "javascript"):
            style = detect_js_style(root, source_dirs or None)
            fw = style.get("framework", "react")
            if fw in ("react", "next"):
                write_react_style_templates(root, out_dir, style)
            elif fw == "vue":
                write_vue_style_templates(root, out_dir, style)
            else:
                write_react_style_templates(root, out_dir, style)
            apply_placeholders(out_dir, placeholders)
        else:
            print(f"[gen-templates-by-rules] unsupported language: {lang}")
            continue
        print(f"[gen-templates-by-rules] generated -> {out_dir}")
        if apply_flag:
            for f in out_dir.rglob("*.tmpl"):
                rel = f.relative_to(out_dir)
                dst = apply_dir / rel
                dst.parent.mkdir(parents=True, exist_ok=True)
                dst.write_text(f.read_text(encoding="utf-8", errors="ignore"), encoding="utf-8")
            print(f"[gen-templates-by-rules] applied -> {apply_dir}")
    return 0


def cmd_custom_init(root: Path, langs: str, style: str) -> int:
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    env_file = root / "config" / "rules.env"
    save_env(env_file, {"INIT_TIMESTAMP": ts, "INIT_LANGS": langs or "python", "INIT_STYLE": style or "mimic"})
    print(f"[custom_init] INIT_TIMESTAMP={ts} -> {env_file}")
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="AI DevFlow Unified CLI")
    p.add_argument("command", help="Command to run")
    p.add_argument("args", nargs="*", help="Additional args for the command")
    p.add_argument("--root", dest="root", default=None, help="Override repository root")
    return p


def main(argv: List[str]) -> int:
    script_path = Path(__file__).resolve()
    root = detect_root(script_path)
    parser = build_parser()
    ns = parser.parse_args(argv)
    if ns.root:
        root = Path(ns.root).resolve()

    cmd = ns.command
    a = ns.args

    try:
        if cmd == "render-rules":
            return cmd_render_rules(root)
        elif cmd == "check-ai-compliance":
            return cmd_check_ai_compliance(root)
        elif cmd == "check-architecture":
            return cmd_check_architecture(root)
        elif cmd == "check-cross-model-ci":
            return cmd_check_cross_model_ci(root)
        elif cmd == "check-doc-sync":
            return cmd_check_doc_sync(root, apply=(a and a[0] == "apply"))
        elif cmd == "check-injection":
            return cmd_check_injection(root)
        elif cmd == "check-session-recovery":
            return cmd_check_session_recovery(root)
        elif cmd == "check-task-docs":
            return cmd_check_task_docs(root)
        elif cmd == "check-templates":
            return cmd_check_templates(root)
        elif cmd == "check-transaction":
            return cmd_check_transaction(root)
        elif cmd == "cross-model-guard":
            return cmd_cross_model_guard(root)
        elif cmd == "framework-evolve":
            mode = a[0] if a else "verify"
            return cmd_framework_evolve(root, mode)
        elif cmd == "gen-mapping-doc":
            return cmd_gen_mapping_doc(root)
        elif cmd == "refactor-rules":
            return cmd_refactor_rules(root)
        elif cmd == "verify-rule-linkages":
            return cmd_verify_rule_linkages(root)
        elif cmd == "list-interface-relations":
            return cmd_list_interface_relations(root)
        elif cmd == "quick-commit":
            msg = " ".join(a) if a else ""
            return cmd_quick_commit(root, msg)
        elif cmd == "validate-commit-msg":
            msg = None
            path = None
            if a:
                for item in a:
                    if item.startswith("msg="):
                        msg = item.split("=", 1)[1]
                    elif item.startswith("file="):
                        path = Path(item.split("=", 1)[1])
            return cmd_validate_commit_msg(root, msg, path)
        elif cmd == "setup-framework-mode":
            return cmd_setup_framework_mode(root)
        elif cmd == "install-git-hooks":
            return cmd_install_git_hooks(root)
        elif cmd == "dev-templates":
            lang = a[0] if len(a) > 0 else "python"
            style = a[1] if len(a) > 1 else "mimic"
            target = Path(a[2]).resolve() if len(a) > 2 else None
            return cmd_dev_templates(root, lang, style, target)
        elif cmd == "gen-style-templates":
            lang = a[0] if len(a) > 0 else "java"
            apply = (len(a) > 1 and a[1] == "apply")
            return cmd_gen_style_templates(root, lang, apply)
        elif cmd == "gen-templates-by-rules":
            return cmd_gen_templates_by_rules(root)
        else:
            print(f"Unknown command: {cmd}")
            return 2
    except Exception as e:
        print(f"[cli] error: {e}")
        return 2


def detect_js_style(root: Path, source_dirs: List[Path] | None = None) -> Dict[str, str | bool]:
    import json
    pkg_json = root / "package.json"
    deps = {}
    if pkg_json.exists():
        try:
            deps = json.loads(pkg_json.read_text(encoding="utf-8", errors="ignore"))
        except Exception:
            deps = {}
    def has_dep(name: str) -> bool:
        for k in ("dependencies", "devDependencies", "peerDependencies"):
            if deps.get(k, {}).get(name):
                return True
        return False
    dirs = [d if d.is_absolute() else (root / d) for d in (source_dirs or [root / "src"])]
    has_ts = bool((root / "tsconfig.json").exists())
    css_modules = False
    styled_components = has_dep("styled-components")
    any_jsx = False
    any_tsx = False
    any_vue = False
    for d in dirs:
        if not d.exists():
            continue
        for f in d.rglob("*.*"):
            if f.suffix in (".tsx",):
                any_tsx = True
            if f.suffix in (".jsx",):
                any_jsx = True
            if f.suffix == ".vue":
                any_vue = True
            if ".module." in f.name and f.suffix in (".css", ".scss", ".sass"):
                css_modules = True
            if f.suffix in (".js", ".jsx", ".ts", ".tsx"):
                t = f.read_text(encoding="utf-8", errors="ignore")
                if "styled-components" in t:
                    styled_components = True
    framework = "react"
    if any_vue or has_dep("vue"):
        framework = "vue"
    elif has_dep("next"):
        framework = "next"
    elif has_dep("react"):
        framework = "react"
    elif has_dep("vite"):
        framework = "react"
    ext = "tsx" if (has_ts or any_tsx) else ("jsx" if any_jsx else "js")
    return {
        "framework": framework,
        "ts": bool(has_ts or any_tsx),
        "ext": ext,
        "css_modules": css_modules,
        "styled_components": styled_components,
    }


def write_react_style_templates(root: Path, out_dir: Path, style: Dict[str, str | bool]) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    ext = str(style.get("ext", "tsx"))
    ts = bool(style.get("ts", True))
    css_modules = bool(style.get("css_modules", False))
    styled = bool(style.get("styled_components", False))
    comp_header = "import React from 'react'\n"
    style_import = ""
    if css_modules:
        style_import = "import styles from './{{ComponentName}}.module.css'\n"
    elif styled:
        style_import = "import styled from 'styled-components'\n\nconst Wrapper = styled.div`/* TODO styles */`\n"
    if styled:
        return_markup = "(<Wrapper>\n    <div>{{ComponentName}}</div>\n  </Wrapper>)"
    elif css_modules:
        return_markup = "(<div className={styles.root}>{{ComponentName}}</div>)"
    else:
        return_markup = "(<div>{{ComponentName}}</div>)"
    component = (
        f"{comp_header}{style_import}\n"
        f"export default function {{ComponentName}}(props{': {}' if ts else ''}) {{\n"
        f"  return {return_markup}\n"
        "}\n"
    )
    hook_ext = "ts" if ts else "js"
    hook = (
        "import { useState, useEffect } from 'react'\n\n"
        f"export function useExample() {{\n  const [state, setState] = useState(null)\n  useEffect(() => {{ /* TODO */ }}, [])\n  return state\n}}\n"
    )
    page = (
        f"{comp_header}\n"
        f"export default function {{PageName}}() {{\n  return (<main><h1>{{PageName}}</h1></main>)\n}}\n"
    )
    files = {
        f"components/Component.{ext}.tmpl": component,
        f"hooks/useExample.{hook_ext}.tmpl": hook,
        f"pages/Page.{ext}.tmpl": page,
    }
    for rel, content in files.items():
        dst = out_dir / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        dst.write_text(content, encoding="utf-8")


def write_vue_style_templates(root: Path, out_dir: Path, style: Dict[str, str | bool]) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    ts = bool(style.get("ts", False))
    lang = "ts" if ts else "js"
    component = (
        f"<script setup lang=\"{lang}\">\n\n</script>\n\n"
        "<template>\n  <div>{{ComponentName}}</div>\n</template>\n\n"
        "<style scoped>\n/* TODO styles */\n</style>\n"
    )
    view = (
        f"<script setup lang=\"{lang}\">\n\n</script>\n\n"
        "<template>\n  <main>\n    <h1>{{ViewName}}</h1>\n  </main>\n</template>\n"
    )
    files = {
        "components/Component.vue.tmpl": component,
        "views/View.vue.tmpl": view,
    }
    for rel, content in files.items():
        dst = out_dir / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        dst.write_text(content, encoding="utf-8")


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
